﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;

namespace CustomNetworking
{
    /// <summary> 
    /// A StringSocket is a wrapper around a Socket.  It provides methods that
    /// asynchronously read lines of text (strings terminated by newlines) and 
    /// write strings. (As opposed to Sockets, which read and write raw bytes.)  
    ///
    /// StringSockets are thread safe.  This means that two or more threads may
    /// invoke methods on a shared StringSocket without restriction.  The
    /// StringSocket takes care of the synchonization.
    /// 
    /// Each StringSocket contains a Socket object that is provided by the client.  
    /// A StringSocket will work properly only if the client refains from calling
    /// the contained Socket's read and write methods.
    /// 
    /// If we have an open Socket s, we can create a StringSocket by doing
    /// 
    ///    StringSocket ss = new StringSocket(s, new UTF8Encoding());
    /// 
    /// We can write a string to the StringSocket by doing
    /// 
    ///    ss.BeginSend("Hello world", callback, payload);
    ///    
    /// where callback is a SendCallback (see below) and payload is an arbitrary object.
    /// This is a non-blocking, asynchronous operation.  When the StringSocket has 
    /// successfully written the string to the underlying Socket, or failed in the 
    /// attempt, it invokes the callback.  The parameters to the callback are a
    /// (possibly null) Exception and the payload.  If the Exception is non-null, it is
    /// the Exception that caused the send attempt to fail.
    /// 
    /// We can read a string from the StringSocket by doing
    /// 
    ///     ss.BeginReceive(callback, payload)
    ///     
    /// where callback is a ReceiveCallback (see below) and payload is an arbitrary object.
    /// This is non-blocking, asynchronous operation.  When the StringSocket has read a
    /// string of text terminated by a newline character from the underlying Socket, or
    /// failed in the attempt, it invokes the callback.  The parameters to the callback are
    /// a (possibly null) string, a (possibly null) Exception, and the payload.  Either the
    /// string or the Exception will be non-null, but nor both.  If the string is non-null, 
    /// it is the requested string (with the newline removed).  If the Exception is non-null, 
    /// it is the Exception that caused the send attempt to fail.
    /// </summary>

    public class StringSocket
    {
        struct SendRequest
        {
            public string Text { get; set; }
            public SendCallback Callback { get; set; }
            public object Payload { get; set; }
        }

        struct ReceiveRequest
        {
            public ReceiveCallback Callback { get; set; }
            public object Payload { get; set; }
        }

        // These delegates describe the callbacks that are used for sending and receiving strings.
        public delegate void SendCallback(Exception e, object payload);
        public delegate void ReceiveCallback(String s, Exception e, object payload);

        // lock synchronization objects 
        private readonly object sendSync = new object();
        private readonly object receiveSync = new object();
        private readonly object sendQueueSync = new object();
        private readonly object receiveQueueSync = new object();
        private readonly object stringLock = new object();

        // these objects truly speak for themselves
        private Socket socket;
        private static System.Text.Encoding encoding;

        // callback queues
        private Queue<SendRequest> sendQueue;
        private Queue<ReceiveRequest> receiveQueue;

        // input/output buffers
        //private byte[] outputBuffer;
        private byte[] inputBuffer;
        private string incoming;
        private string outgoing;

        // send/receive flags
        private bool sendIsGoing = false;
        private bool receiveIsGoing = false;

        private static int BUFFERSIZE = 1024;

        /// <summary>
        /// Creates a StringSocket from a regular Socket, which should already be connected.  
        /// The read and write methods of the regular Socket must not be called after the
        /// LineSocket is created.  Otherwise, the StringSocket will not behave properly.  
        /// The encoding to use to convert between raw bytes and strings is also provided.
        /// </summary>
        public StringSocket(Socket s, Encoding e)
        {
            // initialize member variables
            socket = s;
            encoding = e;
            incoming = "";
            outgoing = "";

            // initialize the callback queues
            sendQueue = new Queue<SendRequest>();
            receiveQueue = new Queue<ReceiveRequest>();
        }

        /// <summary>
        /// We can write a string to a StringSocket ss by doing
        /// 
        ///    ss.BeginSend("Hello world", callback, payload);
        ///    
        /// where callback is a SendCallback (see below) and payload is an arbitrary object.
        /// This is a non-blocking, asynchronous operation.  When the StringSocket has 
        /// successfully written the string to the underlying Socket, or failed in the 
        /// attempt, it invokes the callback.  The parameters to the callback are a
        /// (possibly null) Exception and the payload.  If the Exception is non-null, it is
        /// the Exception that caused the send attempt to fail. 
        /// 
        /// This method is non-blocking.  This means that it does not wait until the string
        /// has been sent before returning.  Instead, it arranges for the string to be sent
        /// and then returns.  When the send is completed (at some time in the future), the
        /// callback is called on another thread.
        /// 
        /// This method is thread safe.  This means that multiple threads can call BeginSend
        /// on a shared socket without worrying around synchronization.  The implementation of
        /// BeginSend must take care of synchronization instead.  On a given StringSocket, each
        /// string arriving via a BeginSend method call must be sent (in its entirety) before
        /// a later arriving string can be sent.
        /// </summary>
        public void BeginSend(String s, SendCallback callback, object payload)
        {
            lock (sendSync)
            {
                sendQueue.Enqueue(new SendRequest { Callback = callback, Payload = payload, Text = s });

                if (!sendIsGoing)
                {
                    sendIsGoing = true;
                    outgoing = s;
                    byte[] outputBuffer = encoding.GetBytes(outgoing);
                    socket.BeginSend(outputBuffer, 0, outputBuffer.Length,
                                     SocketFlags.None, MessageSent, outputBuffer);
                }
            }
        }

        /// <summary>
        /// Checks whether or not all of the pending send requests
        /// have been serviced.
        /// </summary>
        private void MessageSent(IAsyncResult result)
        {
            byte[] outputBuffer;

            // find out how many bytes were sent
            int sentBytes = socket.EndSend(result);

            // find out how many unsent bytes are left
            int remaining = outgoing.Length - sentBytes;

            // send any unsent bytes
            if (remaining > 0)
            {
                outgoing = outgoing.Substring(sentBytes);
                outputBuffer = encoding.GetBytes(outgoing);
                socket.BeginSend(outputBuffer, 0, outputBuffer.Length,
                                 SocketFlags.None, MessageSent, outputBuffer);
            }
            else
            {
                // if the queue is not empty, dequeue the first item and perform callback
                SendRequest request;

                lock (sendSync)
                {
                    request = sendQueue.Dequeue();
                }

                ThreadPool.QueueUserWorkItem(x => request.Callback(null, request.Payload));
                outgoing = outgoing.Substring(request.Text.Length);

                // start another send if the queue is still not empty
                if (sendQueue.Count > 0)
                {
                    outgoing = sendQueue.Peek().Text;
                    outputBuffer = encoding.GetBytes(outgoing);
                    socket.BeginSend(outputBuffer, 0, outputBuffer.Length,
                                 SocketFlags.None, MessageSent, outputBuffer);
                }
                else
                {
                    sendIsGoing = false;
                }
            }
        }

        /// <summary>
        /// We can read a string from the StringSocket by doing
        /// 
        ///     ss.BeginReceive(callback, payload)
        ///     
        /// where callback is a ReceiveCallback (see below) and payload is an arbitrary object.
        /// This is non-blocking, asynchronous operation.  When the StringSocket has read a
        /// string of text terminated by a newline character from the underlying Socket, or
        /// failed in the attempt, it invokes the callback.  The parameters to the callback are
        /// a (possibly null) string, a (possibly null) Exception, and the payload.  Either the
        /// string or the Exception will be non-null, but nor both.  If the string is non-null, 
        /// it is the requested string (with the newline removed).  If the Exception is non-null, 
        /// it is the Exception that caused the send attempt to fail.
        /// 
        /// This method is non-blocking.  This means that it does not wait until a line of text
        /// has been received before returning.  Instead, it arranges for a line to be received
        /// and then returns.  When the line is actually received (at some time in the future), the
        /// callback is called on another thread.
        /// 
        /// This method is thread safe.  This means that multiple threads can call BeginReceive
        /// on a shared socket without worrying around synchronization.  The implementation of
        /// BeginReceive must take care of synchronization instead.  On a given StringSocket, each
        /// arriving line of text must be passed to callbacks in the order in which the corresponding
        /// BeginReceive call arrived.
        /// 
        /// Note that it is possible for there to be incoming bytes arriving at the underlying Socket
        /// even when there are no pending callbacks.  StringSocket implementations should refrain
        /// from buffering an unbounded number of incoming bytes beyond what is required to service
        /// the pending callbacks.
        /// </summary>
        public void BeginReceive(ReceiveCallback callback, object payload)
        {
            lock (receiveQueueSync)
            {
                receiveQueue.Enqueue(new ReceiveRequest { Callback = callback, Payload = payload });
            }

            DoReceiveCallbacks();

            if (!receiveIsGoing & receiveQueue.Count > 0)
            {
                receiveIsGoing = true;
                inputBuffer = new byte[BUFFERSIZE];
                socket.BeginReceive(inputBuffer, 0, inputBuffer.Length,
                                    SocketFlags.None, MessageReceived, inputBuffer);
            }
        }

        /// <summary>
        /// If there are both pending receive requests and lines buffered,
        /// this method will parse the lines and perform any needed callbacks.
        /// </summary>
        private void DoReceiveCallbacks()
        {
            lock (stringLock)
            {
                int index;
                while ((index = incoming.IndexOf('\n')) >= 0 && receiveQueue.Count > 0)
                {
                    ReceiveRequest request;

                    lock (receiveQueueSync)
                    {
                        request = receiveQueue.Dequeue();
                    }

                    string line = incoming.Substring(0, index);
                    ThreadPool.QueueUserWorkItem(x => request.Callback(line, null, request.Payload));
                    incoming = incoming.Substring(index + 1);
                }
            }
        }

        /// <summary>
        /// Called when some data has been received.
        /// </summary>
        private void MessageReceived(IAsyncResult result)
        {
            // get the buffer to which the data was written
            byte[] buffer = (byte[])(result.AsyncState);

            // record the number of bytes received
            int bytes = socket.EndReceive(result);

            // get the latest string from the buffer and append it to incoming
            lock (stringLock)
            {
                incoming += encoding.GetString(buffer, 0, bytes);
            }

            DoReceiveCallbacks();

            // if there are pending receive requests, begin another receive
            if (receiveQueue.Count > 0)
            {
                inputBuffer = new byte[BUFFERSIZE];
                socket.BeginReceive(inputBuffer, 0, inputBuffer.Length,
                                    SocketFlags.None, MessageReceived, inputBuffer);
            }
            else
            {
                receiveIsGoing = false;
            }
        }
    }
}